Skip to content

feat(website): serve the documentation at webjs.dev/docs - #1101

Merged
vivek7405 merged 16 commits into
mainfrom
feat/docs-on-webjs-dev
Jul 25, 2026
Merged

feat(website): serve the documentation at webjs.dev/docs#1101
vivek7405 merged 16 commits into
mainfrom
feat/docs-on-webjs-dev

Conversation

@vivek7405

@vivek7405 vivek7405 commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

Closes #1098

The documentation moves from docs.webjs.dev onto the marketing app at webjs.dev/docs, rendering inside the site's own shell so the header, footer, theme toggle, fonts, and design tokens are literally the same code every other page uses. The only docs-specific chrome left is the page-tree sidebar.

Two reasons. In search, a subdomain accrues authority to itself rather than to webjs.dev, and the docs are the largest body of indexable content the project has (45 routes). In design, the docs carried a parallel layout that had already drifted from the marketing site, so a reader crossing over left one design system and entered another.

docs.webjs.dev becomes a redirect-only service with path-preserving 301s, and it has to keep resolving indefinitely: framework error messages in already-published npm packages point at it, and a published version can never be corrected after the fact.

What changed

  • docs/app/docs/** moved to website/app/docs/** (45 pages plus the sidebar layout, the search endpoint, the llms.txt routes). /docs 308s to /docs/getting-started rather than serving a second landing page at a near-duplicate URL.
  • The docs sub-layout is a non-root layout that writes no shell. It contributes the sidebar, the .prose-docs typography, and docs-scoped metadata, and reads the root layout's design tokens; the docs @theme block and its duplicate font and token declarations are deleted, not merged.
  • The docs read as part of the site, not a panel on it. The sidebar is transparent, so the page's warm glow runs behind both columns instead of stopping at the content edge; the shell shares the header's 1240px container, so the nav lines up with the wordmark above it; and the prose tracks the blog renderer's scale (serif headings in --fg, no rules between sections, code in a --bg-subtle card) instead of the old app's orange h1 and per-h2 rule. Only the nav scrolls, so the search box stays reachable on a long page.
  • One llms.txt. The two apps each published their own; webjs.dev/llms.txt now enumerates every doc page inline, in sidebar order derived from the sidebar itself, instead of pointing at a second index on another host. /llms-full.txt and the per-page markdown routes came along, and both carry X-Robots-Tag: noindex so 45 full-text copies of the docs do not compete with the pages themselves on the domain this change exists to consolidate.
  • The sitemap covers the docs, enumerated from the live pages on disk, so a new doc page is crawlable the moment it exists. docs.webjs.dev served no sitemap at all, which is why this migration was cheap.
  • docs.webjs.dev redirects path-preservingly with a 301, leaving /__webjs/* to the framework so the deploy healthcheck still passes.
  • Hardcoded docs URLs updated in packages/core, packages/server, packages/cli (bin, generator, templates), the VS Code extension, AGENTS.md, README.md, the skill references, and the scaffold's per-agent rule files.

Bugs found on the way

A root middleware.ts has never been loaded. loadMiddleware looked up the single literal middleware.js, so a root middleware written in TypeScript was silently skipped: no error, no warning, indistinguishable from an app that has none. examples/blog/middleware.ts ships in this repo, sets x-webjs: demo on every response, and that header has never been there. Fixed to resolve .ts / .js / .mts / .mjs, with a counterfactual and a cross-runtime proof. I did not work around it: shipping a workaround for a broken documented convention, in the PR about doc drift, is how drift gets normalized.

The search endpoint rooted its index at process.cwd() and imported its walker via ../../../../packages/server/src/fs-walk.js, four levels out of the app into the framework. It now indexes off getDocPages(), the same extraction the llms.txt routes use, so search and the corpus cannot disagree about a page title.

doc-search probed a window.navigate global the client router has never defined, so every search result did a full page reload.

Also fixed: three internal docs links that had been 404ing (/docs/route-handlers, /docs/caching, /docs/advanced), a dead #state fragment, and a counter sample on the progressive-enhancement page that rendered as @click=${'() => this.inc()'}, quoted, which is exactly what invariant 4 forbids, on the page teaching the model.

Deliberately not in this PR

A top-level <script> in a client-router swapped range is never reactivated, so a script emitted as a sibling of swapped content silently never runs. The docs hit this, and the first fix was on this branch. It is reverted and filed as #1102: nothing here depends on it any more (the highlighter moved to the root layout, which is never swapped), and it changes swap semantics, which AGENTS.md rule 1 says needs a browser or e2e assertion rather than the unit test it shipped with. packages/core is down to one changed line on this branch, a docs URL in an error message.

/docs/auth and /docs/authentication ship identical titles and contradict each other about whether the framework ships auth. Pre-existing content, but this PR is what starts submitting the pair to crawlers from a sitemap, so it is filed as #1103.

Test plan

  • Unit + integration: npm test 3323 pass, 0 fail.
  • Website suite: webjs test in website/ 102 pass, 0 fail. New: docs-chrome (header and footer compared byte for byte against a marketing page through the real request pipeline, the skip-link target, docs-scoped metadata, and the social-card fields a shallow merge would drop), docs-sitemap, docs-search, docs-links (every internal /docs href including the sidebar's, every doc page reachable from the nav, the llms ordering, the noindex set).
  • Browser: web-test-runner 24 pass, including a new doc-search suite (debounce, endpoint URL, result rendering, empty state, click interception, supersede).
  • E2E: WEBJS_E2E=1 78 pass, 0 fail.
  • Bun: node scripts/run-bun-tests.js 251 pass, 0 genuine failures. New test/bun/root-middleware.mjs passes under node 26 and bun 1.3, with its own CI step. app-boot.mjs probes real docs routes on both runtimes (it was probing the redirect host's empty 301s, which pass vacuously).
  • Counterfactuals: neutering the middleware resolution reds 5 of 7 tests; reintroducing a docs-only header reds the chrome test; dropping the docs from the sitemap reds 3; breaking the social-card merge reds the metadata test; pointing doc-search at a different endpoint reds the browser test; restoring a process.cwd() root reds the search cwd test; a typo in a sidebar href and an unlinked doc page each red the link test; narrowing the dev-supervisor watch list reds the drift test.
  • Dogfood: website, docs-redirect, and ui-website boot in prod mode with no broken modulepreloads; blog e2e green. Scaffold: webjs create then migrate, boot (/, /features/auth, /examples/todo all 200), and webjs check clean.
  • Manual: verified in a real browser that highlighting survives soft navigation into and within the docs while marketing pages keep their SSR spans, that the drawer opens below the header and its state does not leak across navigation, that no page overflows at 375px, and that the design holds in light and dark at 390, 860, and 1440.
  • webjs check clean on website/ and docs/; tsc --noEmit clean on both.

Doc surfaces

Updated: AGENTS.md, README.md, framework-dev.md, docs/AGENTS.md (rewritten for the redirect host), website/AGENTS.md, packages/server/AGENTS.md, packages/ui/packages/website/AGENTS.md, the skill (SKILL.md plus the routing-and-pages and data-and-actions references), the docs site's own middleware and architecture pages, the scaffold templates and generator, Dockerfile, compose.yaml, .github/workflows/ci.yml, scripts/dev-all.js, both asset generators, package-lock.json, and the .claude skills and hooks that reference doc-surface paths.

N/A: the MCP server (no introspection projection changed, and its knowledge layer reads agent-docs and AGENTS.md, which are updated). The editor plugins (no intellisense, grammar, snippet, or command surface changed; the one VS Code docs URL is updated but the bundle is otherwise untouched).

Follow-up, not in this PR

docs.webjs.dev needs its Railway service redeployed from this branch after merge, and the Cloudflare managed robots.txt block still needs turning off in the dashboard (tracked in #1088). The client-router script bug is #1102; the duplicate auth pages are #1103.

@vivek7405 vivek7405 self-assigned this Jul 25, 2026
@vivek7405

Copy link
Copy Markdown
Collaborator Author

Found while migrating: a root middleware.ts has never been loaded

The plan for docs.webjs.dev was a root middleware that 301s everything, so I wrote docs/middleware.ts and it did nothing. loadMiddleware in packages/server/src/dev.js looked up the single literal middleware.js, so a root middleware written in TypeScript was silently skipped.

Silently is the important part. There is no error and no warning, and an app with a middleware that never loads is indistinguishable from an app that has none. That is why it survived: examples/blog/middleware.ts ships in this repo, sets x-webjs: demo on every response, and I confirmed against the running handler that the header has never been there.

Every other routing convention matches on the file STEM (a per-segment app/x/middleware.ts has always worked), the api scaffold writes middleware.ts, the dev supervisor watches both extensions, and the docs page documents middleware.ts first. This lookup was the one place that disagreed with all of them.

Fixed in 2046bef8, resolving .ts / .js / .mts / .mjs with .ts first. I considered writing the redirect service as middleware.js and filing the bug separately, and decided against it: shipping a workaround around a broken documented convention, in the very PR whose subject is doc/site drift, is how the drift gets normalized. The counterfactual is in the log below.

Verified on both runtimes (test/bun/root-middleware.mjs passes under node 26 and bun 1.3), since this is a module-load path and the TS strip differs per runtime.

@vivek7405

Copy link
Copy Markdown
Collaborator Author

Decision: one llms.txt for the site, not two

The two apps each published their own /llms.txt, and merging them made the split visible. An agent that fetched webjs.dev/llms.txt got a ## Docs section containing a single link to docs.webjs.dev/docs, so reaching an actual doc page took two more hops and knowing that a second index existed.

webjs.dev/llms.txt is now the one index, with every doc page enumerated inline, each linked to its raw-markdown variant (/docs/<topic>/llms.txt) so a model following one gets prose rather than HTML it has to strip. /llms-full.txt moved over as the whole corpus, and the per-page markdown routes came with the pages.

I kept the enumeration derived from the live pages rather than a curated list. Adding a doc page puts it in the index, the sitemap, and the corpus with nothing else to remember, which is the same property the old docs-app index had and the main reason it was worth preserving.

Two things the merge exposed that were already broken:

The search endpoint rooted its index at process.cwd() and imported its walker via ../../../../packages/server/src/fs-walk.js, reaching four levels out of the app into the framework. It now indexes off getDocPages(), the same extraction the llms.txt routes use, so search and the corpus cannot disagree about what a page is called. The cwd bug is pinned by a test that moves the working directory out from under a fresh handler, since webjs test runs the suite from the app dir, the one cwd that could never catch it.

The progressive-enhancement page's counter sample rendered as @click=${'() => this.inc()'}, quoted, because the sample interpolated a real arrow function instead of escaping the hole. Invariant 4 says event holes must be unquoted, so the page teaching progressive enhancement was showing the counterexample.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went through the whole migration: the moved routes, the shared-chrome layout, the redirect host, the middleware fix, and every surface that names the docs URL.

The migration itself holds up. The redirect host has no open-redirect or header-injection hole (WHATWG URL strips CR/LF before parsing and pathname always starts with /, so a //evil.com path resolves onto webjs.dev), invariants 8 and 9 hold in the new sub-layout, and every asset the moved pages need is present and reachable through the auth gate.

Two real bugs, both from the docs no longer owning their own document.

The mobile drawer paints under the site header, because the root layout wraps children in a stacking context and the drawer cannot escape it. The old docs app could not hit this: its header and its drawer were siblings in the same context. Worth remembering as a general hazard for anything the sub-layout wants to float.

code-highlight.js leaks a document.body MutationObserver per navigation into /docs, for the same structural reason: the script moved from a root layout (never swapped) into a sub-layout (inside the router's swap range), and the router re-executes scripts it swaps in. The script had no idempotence guard because it never needed one.

The rest is stale text, and more of it than I expected: five CTAs that kept their new-tab screen-reader cue after becoming same-origin links, plus DOCS_URL and docs.webjs.dev surviving in the agent skill, two hooks, README, and website/AGENTS.md. The .claude/skills/* files got updated but the two hooks that inject the same guidance did not, which is the kind of pair that is easy to half-do.

Comment thread website/app/docs/layout.ts
Comment thread website/public/code-highlight.js
Comment thread .agents/skills/webjs/SKILL.md
Comment thread .claude/hooks/route-skills.sh
Comment thread packages/cli/lib/dev-supervisor.js

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second pass, this time on the tests and the client-side behaviour rather than the file moves, plus the design feedback that the docs still read as pasted in.

The design half came down to background. The sidebar had an opaque fill and a divider rule, so the page's warm glow stopped dead at the content column and the two halves of every docs page had different backgrounds. It was also full-bleed while every other page is centred in the header's 1240px container, and it kept the old app's type voice (orange h1, a rule above every h2) where the site's other long-form surface uses serif in --fg with space instead of rules. All three now match, and only the mobile drawer keeps a fill because it floats over what it covers.

The behavioural half was more interesting, and both bugs share a root cause: the docs stopped owning their own document. Things that were safe in a root layout are not safe in a sub-layout inside the router's swap range, and things that were safe on their own origin are not safe on a shared body element. A top-level script in a swapped range was never reactivated, so entering the docs by soft navigation left every code sample unhighlighted and never installed the observer that would have fixed later navigations. And the drawer's open state, an attribute on body, rode along to whatever page you left for and locked its scrolling.

On the tests, two of mine were weaker than they read. The cwd-independence check could not fail: prod mode does not bust the module cache, so the chdir happened after the modules had already resolved their roots. It runs in a fresh process now. The header/footer equality is real but proves less than its comment claimed, since it always slices the root layout's header; the count assertion beside it is what actually catches a second one. I corrected the comment rather than dressing it up.

Also worth noting: the bun boot check was probing the redirect host's empty 301s, which pass the status check and read zero preloads, so it looked like docs coverage while providing none.

Comment thread packages/core/src/router-client.js Outdated
Comment thread website/app/layout.ts
Comment thread website/app/docs/layout.ts
Comment thread website/components/doc-search.ts
Comment thread docs/middleware.ts
Comment thread test/bun/app-boot.mjs
Comment thread website/test/ssr/docs-search.test.ts

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Third pass, zoomed out to whole-change coherence and the things a per-file read misses. This one was the most useful of the three, because most of what it found is invisible in the diff.

Two guards had gone quiet rather than red, which is worse than breaking. docs-pages-well-formed kept globbing the old app, so it matched one redirect stub with no template in it and ran its balanced-tag check on an empty string. Its tripwire asked for "more than zero" pages, which one satisfies, so nothing complained while all 45 real pages went uncovered. And they matter more than before: the bug that test was written against was an unclosed tag swallowing a client-router marker, and the docs are inside the marketing app's router for the first time. Separately, the doc-sync hook still accepted ^docs/ as a documentation surface, so staging a tsconfig from a three-file redirect host would have satisfied the gate.

The lockfile was the one thing that would have failed the merge outright rather than just rotting: the docs workspace was renamed and npm ci validates that.

The rest was drift I had propagated instead of finishing: both asset generators still wrote into the deleted docs/public, CI step names still advertised docs boot coverage this PR deliberately removed, and README plus the AGENTS.md app-layout block still described the docs as a separate app.

The client-router fix is out of this PR, filed as #1102. Nothing here depends on it any more, since moving code-highlight.js to the root layout means no swapped script needs reactivating. It is a real bug and the fix is small, but it changes swap semantics, and AGENTS.md rule 1 asks for a browser or e2e assertion on a client-router change rather than the unit test it shipped with. That is a PR of its own, not a rider on a docs migration. packages/core is now back to one changed line on this branch, the docs URL in an error message.

Comment thread package-lock.json
Comment thread test/docs/docs-pages-well-formed.test.js
Comment thread scripts/generate-og.mjs
Comment thread .github/workflows/ci.yml
Comment thread README.md
Comment thread .claude/hooks/require-docs-with-src.sh
Comment thread AGENTS.md

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fourth pass, the convergence check: re-verify the previous rounds' fixes, then look at the moved pages themselves and the SEO surface, which is the whole point of the change.

The worst finding is one the migration itself introduced. Deleting the docs' own root layout took its title and description with it, so every one of the 45 pages was serving the landing page's blurb as its search snippet and social card, differing only in <title>. On a change whose entire justification is search, that is the wrong regression to ship, and neither the chrome test nor the repo-health SEO test looked at description or og. The sub-layout carries docs-scoped metadata now.

Three mobile problems, all consequences of the new shell rather than anything inherited. A wide table on /docs/runtime pushed the document past the viewport and dragged the fixed header with it, because pre had an overflow container and table did not. The menu button only gained aria-expanded after its first click, so it announced as a plain button until then and never gained state at all with JS off. And the closed drawer was only translated off-screen, so 45 links sat in the tab order ahead of the content on every docs page.

Three internal docs links have been 404ing since before the move (/docs/route-handlers, /docs/caching, /docs/advanced, none of which are real slugs). Fixing them is a footnote; the guard is the point, so the next one fails in CI rather than shipping.

Last, the per-page markdown routes are full-text copies of pages that also exist as HTML, and text/plain cannot carry a canonical. Indexing both would put 45 near-duplicates of the docs on the exact domain this change exists to consolidate, which is the same reasoning the sitemap already uses to exclude the /docs redirect. They now carry X-Robots-Tag: noindex and stay fetchable, which is what an agent asking for llms.txt actually wants. /llms.txt itself is left indexable: it is a link list, not a copy of anything. (That one is in website/lib/docs-llms.server.ts, which GitHub renders as a rename with no diff, so it has no inline anchor.)

Comment thread website/app/docs/layout.ts Outdated
Comment thread website/app/docs/layout.ts Outdated
Comment thread website/app/docs/layout.ts
Comment thread website/app/docs/layout.ts
Comment thread website/app/docs/file-storage/page.ts
Comment thread website/app/docs/architecture/page.ts
Comment thread test/bun/app-boot.mjs
Comment thread test/docs/docs-pages-well-formed.test.js

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fifth pass, aimed at the most recent commits, because each round's fixes have been the source of the next round's findings and this one is the clearest example.

The metadata fix I shipped last round made the thing it was fixing worse. Metadata merges as a shallow spread per layer, so naming openGraph in the sub-layout REPLACES the root's object instead of merging into it. Every docs URL lost og:image, its dimensions, og:url, and the summary_large_image card, so they shared as bare text cards. The description regression I was fixing was real, but I traded it for a bigger one, and my own comment claimed "the og:image stays the site's, deliberately" while the rendered HTML said otherwise. Worse, the test I wrote alongside it asserted only the fields I had just set, so it passed throughout. That is the lesson worth writing down: a test written from the fix rather than from the risk will confirm whatever you just did.

The table fix had the same shape. It solved the mobile overflow and introduced a desktop one, because display: block also makes a table shrink to fit, so row rules stopped short of the column edge on 15 pages. It only ever needed to apply at the narrow breakpoint.

Two things four rounds had not looked at. At exactly 860 CSS pixels the docs had no navigation at all: my hand-written media query includes 860 and Tailwind's max-[860px]: variant excludes it, so the grid had already collapsed and the button was already hidden while the drawer rules had not taken over. And the link guard I added last round could not see the sidebar, which is the one surface linking all 45 pages, because those hrefs are single-quoted literals rendered through a template hole. It also dropped fragment links entirely instead of checking their path, which is how a dead /docs/components#state walked straight past it.

Smaller but real: the highlighter was the only script in the root layout without a CSP nonce, so turning on the opt-in nonce CSP would have blocked exactly it; the llms 404 path reused the 200's five-minute public cache, so a shared cache would pin a not-found whose body echoes the requested topic; and the corpus builder used raw NUL bytes as its sentinel, which makes git treat a 16KB shared server module as binary and therefore unreviewable.

Comment thread website/app/docs/layout.ts
Comment thread website/app/docs/layout.ts
Comment thread website/app/docs/layout.ts
Comment thread website/test/ssr/docs-links.test.ts
Comment thread website/app/layout.ts
vivek7405 added 12 commits July 25, 2026 23:56
The docs lived on docs.webjs.dev, which cost us twice. In search, a
subdomain accrues authority to itself rather than to webjs.dev, and the
docs are the largest body of indexable content the project has. In
design, they carried their own layout, nav, and footer, so a reader
crossing over from the marketing site left one design system and entered
another.

Moving them onto the marketing app as app/docs/** fixes both at once.
The pages now render inside the site's own shell, so the header, footer,
theme toggle, fonts, and design tokens are literally the same code the
rest of the site uses. The only docs-specific chrome left is the
page-tree sidebar.

docs.webjs.dev becomes a redirect-only service. It has to keep resolving
indefinitely, because framework error messages in already-published npm
packages point at it and a published version can never be corrected
after the fact.
The root middleware lookup was the single literal `middleware.js`, so an
app whose root middleware was written in TypeScript never had it loaded.
There was no error and no warning: a missing middleware looks exactly
like an app that has none, which is why this went unnoticed even though
examples/blog ships a root middleware.ts that has never run.

TypeScript is the documented default for an app, the scaffold writes
middleware.ts for the api template, the dev supervisor watches both
extensions, and every other routing convention matches on the file stem.
This was the one lookup that did not.

Resolves .ts, .js, .mts, and .mjs, .ts first to match the supervisor's
order. Found while making docs.webjs.dev a redirect-only host, whose
whole behaviour is a root middleware.
The documentation URL changes with the move, so every place that names
it has to move with it: the error messages the framework throws, the
scaffold's generated footer and agent rule files, AGENTS.md, README, the
skill references, and the tests that anchor on those strings.

The error-message URLs in core and server can only be corrected going
forward. Anyone running an already-published version keeps getting the
old host, which is exactly why docs.webjs.dev has to keep redirecting
rather than being retired.

Two smaller things fixed on the way. The UI website derived its docs
link from its own DOCS_URL env var, which is now one service variable
that can drift from WEBSITE_URL for no reason, so it reads
WEBSITE_URL + /docs instead. And the progressive-enhancement page's
counter sample rendered as @click=${'() => this.inc()'}, quoted, which
is the exact thing invariant 4 forbids, on the page teaching the model.
The move is easy to half-undo without anything looking wrong: someone
reintroduces a docs-only header, or adds a doc page that never reaches
the sitemap, and the pages still render fine. These are the assertions
that make those fail.

Shared chrome is asserted by comparing the rendered header and footer of
a doc page against a marketing page byte for byte, through the real
request pipeline rather than the layout function, since 'the docs end up
wrapped in the root layout' is the property under test.

The search endpoint gains its first tests. Its index was rewritten to
reuse the llms.txt extraction, which removed a process.cwd() root and an
import that reached four levels out of the app into packages/. The cwd
one is pinned by moving the working directory out from under a fresh
handler, because the suite otherwise runs from the one directory that
cannot catch it.

Also repoints the doc tests, the api-coverage corpus, and the knowledge
and preload sweeps at the app that serves the docs now.
The docs host renders no HTML now, only redirects, so it has no
public/input.css to compile and the image build step for it would fail.
The website carries the docs, so its DOCS_URL cross-link is gone, and
the docs service gains SITE_URL, which is where it sends every request.
Review turned up nine things. The two that matter to a reader:

The mobile docs drawer painted UNDER the site header. The root layout
wraps children in a relative z-1 element, which is a stacking context,
so nothing the docs layout renders can rise above the z-20 header no
matter what z-index it claims. The old docs app did not hit this because
its header lived in the same context as its drawer. The drawer now opens
below the header instead of trying to cover it, which sidesteps the
stacking context and leaves the header usable while it is open.

code-highlight.js attached a document.body MutationObserver every time
it ran, and it now sits inside the client router's swap range, so each
marketing-to-docs navigation leaked another one. Guarded.

The rest were stale text: five docs CTAs kept a screen-reader 'opens in
a new tab' cue after becoming same-origin links, and DOCS_URL and
docs.webjs.dev survived in the agent skill, two hooks, README, and
website/AGENTS.md.

Also widens the dev supervisor's middleware watch list to the four
extensions the server now loads, with a test that reads the server's
list rather than restating it, since a middleware that loads but never
triggers a dev restart is the same quiet failure in a new place.
The docs still read as a section pasted into the site rather than part
of it, and the reason was mostly background. The sidebar carried an
opaque fill and a divider rule, so the page's warm glow stopped at the
content column and the two halves had different backgrounds. It was also
full-bleed while every other page is centred in a 1240px container, and
it kept the old docs app's type voice: an orange h1 and a rule above
every h2, where the site's other long-form surface uses serif headings
in --fg with space instead of rules.

The sidebar is transparent now, the shell shares the header's container
so the nav lines up with the wordmark above it, and the prose tracks the
blog renderer's scale. Only the mobile drawer keeps a fill, because it
floats over the content it covers.

Also from review: code-highlight.js never ran when a reader entered the
docs by soft navigation. It sat in the docs sub-layout, inside the
router's swap range, and a top-level script in a swapped range was never
reactivated. Fixed in the router, and the script moved to the root
layout (never swapped) scoped to .prose-docs, so it no longer depends on
swap mechanics at all. Marketing pages keep the spans lib/highlight.ts
already emitted at SSR.

The drawer's open state lived on <body>, outside every swap range, so it
rode along to the next page and locked its scrolling. Cleared from the
root layout, which survives navigation.

Smaller: doc-search probed a window.navigate global the router has never
defined, so every search result did a full reload; the redirect host
answered /__webjs/core/* with a canned body; the docs main had no
skip-link target and then no tabindex; and the bun boot check probed the
redirect host's empty 301s while believing it covered the docs.
The lockfile still recorded the docs workspace under its old name, which
npm ci validates, so CI would have failed on it.

Two guards had gone quiet rather than red. docs-pages-well-formed still
globbed docs/app, which now matches one redirect stub with no template
in it, so the balanced-tag check ran on an empty string and passed while
covering none of the 45 real pages; its tripwire said 'more than zero',
which that satisfies, so it now asserts a real floor. And the doc gate
hook still accepted ^docs/ as a documentation surface, where staging a
tsconfig from a three-file redirect host would satisfy it.

Both favicon and OG generators still wrote into the deleted docs/public,
and CI step names, README, and the AGENTS.md app-layout block still
described the docs as a separate app.

The client-router fix is reverted out of this PR and filed as #1102.
Nothing here depends on it any more: code-highlight.js moved to the root
layout, so it no longer needs a swapped script to be reactivated. It is
a real bug, but it changes swap semantics and AGENTS.md rule 1 asks for
a browser or e2e assertion on a client-router change, which is more than
belongs in a docs migration.
Dropping the docs' own root layout took its title and description with
it, so all 45 pages were advertising the landing page's blurb as their
search snippet and social card, with only the title differing. That is a
regression the migration introduced, and on an SEO-motivated change it is
the wrong one to ship. The sub-layout now carries docs-scoped metadata;
pages still own their titles.

Three mobile problems, all from the new shell. A wide table pushed the
whole document sideways on /docs/runtime, taking the fixed header with
it, because pre had an overflow container and table did not. The menu
button gained aria-expanded only after its first click, so it announced
as a plain button until then and never gained state with JS off. And the
closed drawer was merely translated off-screen, so 45 links stayed in the
tab order in front of the content on every docs page.

Also: three internal docs links pointed at slugs that do not exist
(route-handlers, caching, advanced), shipping 404s since before the move.
Fixed, with a test that walks every internal /docs href through the real
handler so the next one fails instead of shipping.

And the per-page markdown routes now carry X-Robots-Tag: noindex. They
are full-text copies of pages that also exist as HTML, and text/plain
cannot carry a canonical, so indexing both would put 45 near-duplicates
on the exact domain this whole change exists to consolidate. They stay
fetchable, which is what an agent asking for llms.txt wants. The site
index at /llms.txt is deliberately left indexable.
The metadata fix in the last commit made things worse before better.
Metadata merges as a shallow spread per layer, so the sub-layout's
openGraph object REPLACED the root's rather than merging into it, and
every docs URL lost og:image, its dimensions, og:url, and the
summary_large_image card. Restating every field is the fix; the test now
asserts the image and card type survive, which is what it should have
checked in the first place rather than only the fields I had just set.

The table overflow fix was applied at every width, so a table also
shrank to fit its content on desktop and its row rules stopped short of
the column edge on 15 pages. Scoped to the narrow breakpoint, where it
is the only thing that stops a wide table dragging the fixed header off
screen.

At exactly 860px the docs had no navigation at all: the hand-written
media query includes 860 while Tailwind's max-[860px] variant excludes
it, so the grid had collapsed and the button was hidden while the drawer
rules had not taken over.

The link guard could not see the sidebar, which is the only surface that
links all 45 pages, because its hrefs are single-quoted literals in a
template hole. It also dropped fragment links whole rather than checking
their path, which is how a dead /docs/components#state survived it.

Smaller: the highlighter had no CSP nonce, so enabling the opt-in nonce
CSP would have blocked exactly it; the llms 404 path reused the
5-minute public cache, so a shared cache would pin a not-found whose
body echoes the requested topic; and the corpus builder used raw NUL
bytes as a sentinel, which made git treat a 16KB shared server module as
binary and therefore unreviewable.
The previous commit said the test covers og:image and the card type. It
did not; I added the assertion after the fact rather than before. It
asserts the fields a shallow metadata merge would silently drop, which
is the failure that actually happened, and the counterfactual confirms
it fails when the merge is broken again.
The whole sidebar scrolled, so the search field scrolled out of reach on
a long page and the scrollbar ran the column's full height, pressing
against the field. The aside is a column now: the search box is pinned
and only the link list below it scrolls, with the scrollbar inset off
the links.

The links also pulled themselves back out with a negative margin, so
their text sat flush against the container edge and the hover pill bled
past it. Dropping it indents the text by the link's own padding and
lines the pill up with the sidebar edge.
@vivek7405
vivek7405 force-pushed the feat/docs-on-webjs-dev branch from 6f4d996 to bc6f528 Compare July 25, 2026 18:27
The order was a hand-copied list and it had already drifted: it carried
a slug that no longer exists and was missing six real pages, so the
AI-facing index put Runtime and Security in an alphabetical tail rather
than in Getting Started and Infrastructure. Deriving it from the sidebar
removes the failure mode instead of correcting one instance of it. It
parses the layout source rather than importing it, since the layout
pulls in browser components and this module is server-only.

Also adds the assertion that actually rots in the other direction: the
sitemap and llms.txt both enumerate from disk, and /docs is a redirect
rather than a listing, so the sidebar is the only human path into a doc
page. A new page would land in both indexes while being unreachable by a
reader, with every test green.

The Bun proof gets its own CI step, matching the convention the other
listener proofs use, so its own comment is no longer describing a step
that did not exist. That matters because denylist entries in the Bun
runner justify skipping a file by pointing at exactly such a step.

The /docs/auth and /docs/authentication pages ship identical titles and
contradict each other about whether the framework ships auth. Pre-existing,
but this PR is what starts submitting the pair to crawlers, so it is
filed as #1103 rather than fixed here.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sixth pass, late-stage: whole-diff coherence, whether the test SET covers the real risk surface, a functional sweep against the running server, and merge readiness.

The blocker was that the branch conflicted with main. 8ce50392 modified docs/app/docs/deployment/page.ts while this PR deletes it, and GitHub reported CONFLICTING. The content risk was the real part: resolving it the obvious way (delete wins, since the file moved) would have dropped the X-Forwarded-Proto parity line and the whole "embedding moves the proxy-header responsibility to you" caveat from the live docs, which is a security-relevant deploy warning. Rebased; git's rename detection carried the change onto the moved file, and I verified website/app/docs/deployment/page.ts is now byte-identical to main's version rather than assuming it.

The test-set gap was a direction I had not covered. The sitemap and llms.txt both enumerate topics from disk, and /docs is a redirect rather than a listing page, so the sidebar is the only surface a human can navigate from. A new doc page would appear in the sitemap and the AI index automatically while being unreachable by a reader, with every test green. That is now asserted, and the counterfactual (drop a page in without a nav entry) fails it.

The CURATED_ORDER list in the corpus builder had already drifted before this PR: one slug that no longer exists, six real pages missing, so /llms.txt listed Runtime and Security in an alphabetical tail. I derived it from the sidebar rather than correcting the list, since correcting it just resets the clock on the same drift. (That is in website/lib/docs-llms.server.ts, which GitHub renders as a rename with no diff, so it takes no inline anchor.)

Also fixed a comment that described a CI step which did not exist, and added the step, because the Bun runner's denylist entries justify skipping files by pointing at exactly that kind of dedicated step.

One thing I did not fix here: /docs/auth and /docs/authentication ship identical titles and open by contradicting each other about whether the framework ships auth. It is pre-existing content, but this PR is what starts submitting the pair to crawlers from a sitemap, so it is filed as #1103 rather than folded in.

Comment thread website/test/ssr/docs-links.test.ts
Comment thread test/bun/root-middleware.test.mjs

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seventh pass, and this one is clean.

Checked the rebase is honest rather than assuming it: the pre-rebase and post-rebase diffs are byte-identical apart from blob hashes and hunk offsets, 8ce5039 is an ancestor of HEAD, and both of its deployment-page edits survived the move into the website app. Its listener and forwarded-proto work is untouched by this branch.

Checked the newest commit's edge cases: an empty layout parse degrades to alphabetical and is caught by the ordering test, an unlinked topic ranks to the tail and is caught by the orphan test, and the two caches are independent. The live /llms.txt order matches the sidebar exactly.

Everything green: website 102 node plus 24 browser, test/docs 29, preload-subset, repo-health, the root-middleware resolution tests, app-boot on both runtimes, and webjs check in both apps.

On the deploy: the Dockerfile drops only the deleted docs stylesheet, and a real Tailwind build of the website picks up the docs-only classes, so the merged docs are styled in the image. Compose, the Railway start commands and the healthcheck paths are unchanged, and the docs service has no variables set so SITE_URL correctly falls back to the production origin.

One note for whoever picks this up next, not a defect: booting docs/ from this worktree through the symlinked CLI resolves @webjsdev/server from the primary checkout, which lacks the middleware fix, so deep paths 404. That is a worktree artifact; the behaviour is correct through createRequestHandler and in the deploy.

@vivek7405
vivek7405 marked this pull request as ready for review July 25, 2026 18:47
The section label had no horizontal padding while the links had 12px, so
the label sat further left than the items under it. Both carry the same
small inset now, which is what makes the label read as a heading for the
group rather than a separate column.
Every list in the docs rendered as indented plain text with no bullets
or numbers: Tailwind's preflight sets list-style: none on ul/ol, and the
prose rules re-added only the padding. Without a visible marker the 24px
inset read as an arbitrary, inconsistent indent (the report that exposed
it was 'why is this section indented', on a numbered how-it-works list
whose numbers were invisible).

With markers restored the indentation rule across the docs is simple and
uniform: text sits flush unless it carries a marker, and the inset exists
only to house that marker.

Also fixes the description extractor stopping dead at an escaped quote,
which published the garbage fragment 'WebJs pages and components are
SSR\' as the progressive-enhancement entry in /llms.txt and the search
index, plus a nav comment describing padding values that were never the
ones on the element.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eighth pass, a fresh full read of the whole diff after the padding commit.

Three things, and the interesting one only looked cosmetic. The report was "why is this section indented", on the suspense page's how-it-works list. The actual cause: Tailwind's preflight sets list-style: none on every ul/ol, and the prose rules re-added only the padding, so every list in the docs has been rendering as indented plain text with NO bullets or numbers. The numbered steps on that page were a numbered list whose numbers were invisible, at which point the indent genuinely is arbitrary. Markers are restored now, which makes the indentation rule across the docs uniform: text sits flush unless it carries a marker, and the inset exists only to house the marker. A guard asserts the restatement stays, since deleting it renders fine, just wrong, and nothing else would notice.

The second is in the corpus extractor: the description regex stopped dead at an escaped quote, so the progressive-enhancement page, whose description contains SSR\'d, published the garbage fragment "WebJs pages and components are SSR" as its entry in /llms.txt and fed the same fragment to the search index. Fixed to cross escapes and unescape the capture, with a test asserting the known page is whole and that no index line anywhere ends in a backslash, so the next escaped quote fails loudly. (The extractor lives in website/lib/docs-llms.server.ts, a rename GitHub shows with no diff, so it takes no inline anchor.)

Third was a comment on the sidebar nav describing padding values that were never on the element, wrong at birth and made more wrong by the padding commit next to it. Corrected to describe what is actually there.

The padding commit itself is sound: the label and links share the inset, the utilities exist in the compiled stylesheet, and the live render matches.

Comment thread website/app/docs/layout.ts
Comment thread website/app/docs/layout.ts
The list was a ul with numbers hand-written into the item text, which
the invisible-marker era let pass as a plain numbered list. With markers
restored it rendered a bullet stacked on each number. A real ol is also
what a screen reader or crawler should have been given all along.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ninth pass, converging. Verified the escape-crossing regexes (unescape order relative to entity decoding is safe, no metadata uses control escapes that would mis-fold), the marker CSS in both themes and on the one nested list, and the suites.

One finding: the routing page's route-resolution order was a ul with the numbers hand-written into the item text. The invisible-marker era let it pass as a plain numbered list; restoring markers rendered a bullet stacked on each number. It is now a real ol with the hand numbers dropped, which is also what a screen reader or a crawler should have been handed all along. Swept the rest of the site for the same pattern; this was the only instance.

Comment thread website/app/docs/routing/page.ts

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tenth pass, clean.

Confirmed the ol conversion on the live page: one ordered list, four decimal-numbered items, no hand-written numbers left, and a repo-wide sweep found no other page with the old pattern. Website suite green (103 server, 24 browser), the routing page renders with zero console errors, and the key routes (/docs redirect, /llms.txt, /sitemap.xml) all answer correctly.

Nothing left to flag. The two follow-ups on the board are #1102 (swapped-range script reactivation) and #1103 (the duplicate auth pages).

@vivek7405
vivek7405 merged commit e07ea3a into main Jul 25, 2026
10 checks passed
@vivek7405
vivek7405 deleted the feat/docs-on-webjs-dev branch July 25, 2026 19:32
vivek7405 added a commit that referenced this pull request Jul 26, 2026
core 0.7.45 ships the #1114 fix (a stale hover prefetch no longer degrades
a click to a full page load) plus the new webjs:navigation-fallback event.

The two editor plugins were missed by my first audit and are added here.
I had dismissed them with "intellisense is unchanged, so neither plugin
owes a republish", which is the wrong test: the question is whether the
PLUGIN's own shipped bundle changed since the PLUGIN's last release, not
since intellisense's. Both did.

The VS Code extension is the sharper case. Its shipped "open documentation"
command still points at docs.webjs.dev, which moved onto the main domain in
#1101. That is user-visible behaviour in a published artifact, and it is the
same class of stale-host fix that core 0.7.44 was released on its own to
ship. webjs.nvim carries a refreshed committed intellisense bundle.

Both changelogs keep npm: false, so release.yml cuts GitHub Releases for
them and publishes neither to the registry. The actual distribution is still
manual: the nvim subtree split, and a vsce publish by the owner.
vivek7405 added a commit that referenced this pull request Jul 26, 2026
core 0.7.45 ships the #1114 fix (a stale hover prefetch no longer degrades a
click to a full page load) plus the new webjs:navigation-fallback event.

Both editor plugins were missed by my first audit pass. I had dismissed them
with "intellisense is unchanged, so neither plugin owes a republish", which
is the wrong test: the question is whether the PLUGIN's own shipped bundle
changed since the PLUGIN's last release, not since intellisense's.

The VS Code extension is the sharper case. Its shipped "open documentation"
command still pointed at docs.webjs.dev, which moved onto the main domain in
#1101, and its README (packaged into the vsix, rendered as the Marketplace
listing) carried a brand-casing fix. Both are user-visible in a published
artifact, and the stale-host one is the same class core 0.7.44 was released
on its own to ship.

webjs.nvim carries a committed copy of @webjsdev/intellisense. Its src was
already current, but the copied package.json still declared 0.5.1 against a
real 0.5.4, so the published plugin misreported the language-service version
it ships. Re-vendored here. Worth noting the drift guard cannot catch this:
vendor-sync.test.mjs byte-compares only src/, while the vendor script also
copies package.json.

Distribution note, stated correctly this time: both plugin changelogs carry
npm: false, and BOTH publish-npm.js and publish-release.js skip on that flag.
So merging cuts no npm publish, no GitHub Release and no tag for the two
plugins; their only automated output is the /changelog entry. Actual
distribution stays manual: the nvim subtree split, and a vsce publish by the
owner.
vivek7405 added a commit that referenced this pull request Jul 26, 2026
core 0.7.45 ships the #1114 fix (a stale hover prefetch no longer degrades a
click to a full page load) plus the new webjs:navigation-fallback event.

Both editor plugins were missed by my first audit pass. I had dismissed them
with "intellisense is unchanged, so neither plugin owes a republish", which
is the wrong test: the question is whether the PLUGIN's own shipped bundle
changed since the PLUGIN's last release, not since intellisense's.

The VS Code extension is the sharper case. Its shipped "open documentation"
command still pointed at docs.webjs.dev, which moved onto the main domain in
#1101, and its README (packaged into the vsix, rendered as the Marketplace
listing) carried a brand-casing fix. Both are user-visible in a published
artifact, and the stale-host one is the same class core 0.7.44 was released
on its own to ship.

webjs.nvim carries a committed copy of @webjsdev/intellisense. Its src was
already current, but the copied package.json still declared 0.5.1 against a
real 0.5.4, so the published plugin misreported the language-service version
it ships. Re-vendored here. Worth noting the drift guard cannot catch this:
vendor-sync.test.mjs byte-compares only src/, while the vendor script also
copies package.json.

Distribution note, stated correctly this time: both plugin changelogs carry
npm: false, and BOTH publish-npm.js and publish-release.js skip on that flag.
So merging cuts no npm publish, no GitHub Release and no tag for the two
plugins; their only automated output is the /changelog entry. Actual
distribution stays manual: the nvim subtree split, and a vsce publish by the
owner.
vivek7405 added a commit that referenced this pull request Jul 26, 2026
core 0.7.45 ships the #1114 fix (a stale hover prefetch no longer degrades a
click to a full page load) plus the new webjs:navigation-fallback event.

Both editor plugins were missed by my first audit pass. I had dismissed them
with "intellisense is unchanged, so neither plugin owes a republish", which
is the wrong test: the question is whether the PLUGIN's own shipped bundle
changed since the PLUGIN's last release, not since intellisense's.

The VS Code extension is the sharper case. Its shipped "open documentation"
command still pointed at docs.webjs.dev, which moved onto the main domain in
#1101, and its README (packaged into the vsix, rendered as the Marketplace
listing) carried a brand-casing fix. Both are user-visible in a published
artifact, and the stale-host one is the same class core 0.7.44 was released
on its own to ship.

webjs.nvim carries a committed copy of @webjsdev/intellisense. Its src was
already current, but the copied package.json still declared 0.5.1 against a
real 0.5.4, so the published plugin misreported the language-service version
it ships. Re-vendored here. Worth noting the drift guard cannot catch this:
vendor-sync.test.mjs byte-compares only src/, while the vendor script also
copies package.json.

Distribution note, stated correctly this time: both plugin changelogs carry
npm: false, and BOTH publish-npm.js and publish-release.js skip on that flag.
So merging cuts no npm publish, no GitHub Release and no tag for the two
plugins; their only automated output is the /changelog entry. Actual
distribution stays manual: the nvim subtree split, and a vsce publish by the
owner.
vivek7405 added a commit that referenced this pull request Jul 26, 2026
core 0.7.45 ships the #1114 fix (a stale hover prefetch no longer degrades a
click to a full page load) plus the new webjs:navigation-fallback event.

Both editor plugins were missed by my first audit pass. I had dismissed them
with "intellisense is unchanged, so neither plugin owes a republish", which
is the wrong test: the question is whether the PLUGIN's own shipped bundle
changed since the PLUGIN's last release, not since intellisense's.

The VS Code extension is the sharper case. Its shipped "open documentation"
command still pointed at docs.webjs.dev, which moved onto the main domain in
#1101, and its README (packaged into the vsix, rendered as the Marketplace
listing) carried a brand-casing fix. Both are user-visible in a published
artifact, and the stale-host one is the same class core 0.7.44 was released
on its own to ship.

webjs.nvim carries a committed copy of @webjsdev/intellisense. Its src was
already current, but the copied package.json still declared 0.5.1 against a
real 0.5.4, so the published plugin misreported the language-service version
it ships. Re-vendored here. Worth noting the drift guard cannot catch this:
vendor-sync.test.mjs byte-compares only src/, while the vendor script also
copies package.json.

Distribution note, stated correctly this time: both plugin changelogs carry
npm: false, and BOTH publish-npm.js and publish-release.js skip on that flag.
So merging cuts no npm publish, no GitHub Release and no tag for the two
plugins; their only automated output is the /changelog entry. Actual
distribution stays manual: the nvim subtree split, and a vsce publish by the
owner.
vivek7405 added a commit that referenced this pull request Jul 27, 2026
#1101 widened the documentation from the 1200 container and 800 content
column it used on its own domain to 1240/820, and this branch inherited
that and then applied it to the gallery as well. Back to 1200/800 for
both, from the one shared shell.

A reference page is read line by line and 800px is the comfortable
ceiling for that measure; the extra 20px bought nothing. Note the shell
is now 40px narrower than the header's own container, so the sidebar
sits 20px inside the wordmark rather than flush with it. That is the
deliberate trade, recorded in the shell, and the header can follow if
the offset ever reads as an accident.
vivek7405 added a commit that referenced this pull request Jul 27, 2026
… 800

Splits the two decisions #1101 had moved together. The outer container
goes back to the header's 1240 so the sidebar sits flush under the
wordmark, while the content column stays capped at 800, the comfortable
ceiling for a reference page read line by line.

The slack lands as space between the sidebar and the text rather than as
a longer line. Verified at 1600px: sidebar and wordmark both start at
204, content column 800, no horizontal scroll.
vivek7405 added a commit that referenced this pull request Jul 27, 2026
Reverts the two width commits from this session, putting the container
back to 1240 and the content column to 820, byte-identical to what main
serves today.

Those changes were chasing a widening that happened in #1101, not here,
and the second of them (820 to 800) was a 20px difference nobody could
see. The shell is back to inheriting whatever the documentation uses, so
/docs is untouched by this PR and /ui matches it, which was the original
request.
vivek7405 added a commit that referenced this pull request Jul 27, 2026
* refactor(website): extract the docs shell into lib/docs-shell.ts

The sidebar, mobile drawer, and .prose-docs typography move out of the
docs sub-layout into a shared shell so the component library at /ui
(#1099) can render the exact same chrome instead of a drifting copy.
No DOM change for /docs; the source-reading chrome guards now point at
the shell file.

* feat(website): mirror the ui registry into the site for /ui previews

scripts/copy-registry.mjs copies packages/ui/packages/registry components
into the gitignored components/ui/ and lib/ui/, rewriting the kit-local
lib imports, so the upcoming /ui gallery pages (#1099) can import the
components for SSR + live previews. Wired into webjs.dev.before /
webjs.start.before, with the registry sources added to the Tailwind
regenerate inputs.

* feat(website): serve the UI component gallery at /ui

The gallery moves off ui.webjs.dev into the marketing app, rendering in
the same shell as /docs: shared header, footer, theme toggle, fonts, and
tokens, with the component list in the sidebar under plain Tier 1 and
Tier 2 headers. There is no landing page; /ui opens on the introduction
the way /docs opens on Getting Started.

Preview panes carry the kit's neutral shadcn palette, scoped to
.ui-preview so a demo looks like the reader's own app while the page
around it stays webjs.dev. Those tokens sit in a @theme inline block
because a plain @theme resolves its var() at :root, where the kit
palette does not exist, which renders every filled component
transparent.

* docs(website): scope the UI kit copy to WebJs apps

The page claimed the CLI detects Next, Astro, Vite, Lit, or plain
projects and adapts. Detection covers four types and neither Lit nor
SvelteKit is among them, and all it picks is the stylesheet path and the
alias set written into components.json. Dropping the cross-framework
pitch rather than describing it more precisely: the kit is for WebJs
apps.

* docs(website): match the docs title shape on the UI gallery

Component pages titled '<Name> | WebJs UI' rather than '<name>: WebJs
UI', so a tab or a search result reads the same coming from /docs or
/ui. The registry identifier stays lowercase where it is the thing you
type.

* docs(website): capitalize the component heading, drop the back link

The sidebar lists every component on every page, so the 'All components'
control above each title was a second way to do what the nav already
does. The heading now reads as a name (Alert Dialog) while the registry
identifier stays lowercase wherever it is a value you type.

* feat(website): serve the ui registry API at /ui/registry

The three endpoints shipped @webjsdev/ui versions fetch from
(/registry, /registry/index.json, /registry/<name>.json) now answer under
/ui as well, ahead of turning ui.webjs.dev into a redirect host. Shared
headers live in one module so the three cannot drift on caching or CORS.

Verified by running the real published webjsui 0.3.8 binary against
http://localhost:5001/ui/registry: init wrote components.json and the
theme, add fetched and wrote a genuine components/ui/button.ts.

* feat(ui-website): turn ui.webjs.dev into a redirect-only host

The gallery and the registry API now live at webjs.dev/ui, so this app
drops its pages, layout, chrome, assets, and registry composer and keeps
only a middleware that permanently redirects.

The mapping is path-aware rather than a blind prefix: /registry/** keeps
its shape under /ui, /docs/components/<name> flattens to /ui/<name>, and
/ and /docs both land on /ui. A prefix redirect would have sent every
existing component link to /ui/docs/components/<name>, which 404s.

/registry is a live API for already-published packages, so the redirect
also carries CORS, and test/ui/ui-host-redirect.test.mjs asserts
destinations rather than the presence of a redirect.

* feat(website): put the gallery in the sitemap and retire the docs/ui page

The sitemap enumerates one URL per component from the live registry
index, so a component added to the registry is crawlable the moment it
exists. Themes and lib items are excluded: they have no page.

/docs/ui was a second hand-written description of the same kit and had
drifted badly, claiming roughly 55 components against an actual 32 and
showing a <ui-button> API the kit does not have. It is deleted and its
URL permanently redirects to /ui, so one page owns the keyword.

* test: repoint the ui suites at the gallery's new home

The composer, the Dockerfile bake guard, the preload probe, and the SEO
tag checks all pointed at files the migration moved or deleted. The
guards themselves still matter, so they follow the code rather than
being dropped: the preload probe now covers /ui and /ui/button, and the
SEO checks cover only apps that still serve HTML.

Adds a bake-ordering assertion (copy-registry before Tailwind) that
nothing covered: the mirrored sources are a scanned @source, so a
stylesheet built first is missing every kit utility class and the
previews render unstyled in production only. Also pins
HOSTED_REGISTRY_URL, which selects local-first resolution.

* test: point the ui touch e2e and CI boot checks at /ui

The touch e2e booted the ui-website and read /docs/components/<name>,
which the migration emptied, so it would have failed against a host that
now only redirects. It boots the marketing app and reads /ui/<name>
instead; all 5 checks pass, including the soft-nav round trip that
exercises the client router across gallery links.

The Bun and Node boot checks drop the two redirect-only hosts, whose
routes are empty 301s that pass a status check vacuously, and cover /ui
and /ui/button on the app that actually serves them.

* docs: sync every surface with the gallery's move to webjs.dev/ui

Root README, framework-dev, AGENTS.md, packages/ui/AGENTS.md, the blog
example's docs, and the start-work skill all described a ui-website that
serves pages. It serves redirects now, so they point at the marketing
site instead, and the mirror footgun section is rewritten: the scope
narrowed to the ui/ subdirectories, so a hand-written site component no
longer needs the app/_components workaround.

Leaves packages/ui/README.md's project-detection claim alone on purpose.
It is wrong (it advertises Lit detection that does not exist), but the
fix belongs with removing the behaviour, tracked as #1129.

* docs(website): stop the tier map tripping the component scanner

The JSDoc described a Tier-2 component with a literal class-declaration
snippet, and the dev server's orphan-component scan reads source text
without skipping comments, so every boot warned that a file declaring no
components had an unregistered one. Rewritten as prose.

* fix: untrack stray files, resync the lock, and repair the redirect gaps

Review found seven real problems.

A `git add -A` swept in a self-referential node_modules symlink and
three screenshots, all pre-existing working-tree junk that would
materialize as an ELOOP path in any fresh clone.

The lockfile still listed the two dependencies the redirect host no
longer needs, so `npm ci` would have failed in CI.

The redirect sent asset URLs under /ui, where they 404. The old root
layout published /public/og.png as its og:image and the favicons beside
it, so every social card already scraped from that host pointed there;
assets now keep their path, which resolves to the equivalent file on the
new host, with a test per URL.

dev-all never passed SITE_URL to the ui host, so `npm run dev:all` made
localhost:5003 redirect to production instead of the local site.

The Tailwind regenerate step scanned a stale mirror: the generated
sources are a scanned @source, so it now refreshes the mirror first or
the rebuild silently omits every kit utility class.

website/AGENTS.md documented none of app/ui, lib/docs-shell, modules/ui,
or the generated mirror, and still told deployers to set UI_URL, which
no longer exists.

* chore: ignore stray root images and a node_modules symlink

The node_modules rule had a trailing slash, so it matched directories
only and a symlink of that name sailed past it into a commit. A
self-referential one resolves to an ELOOP path that npm and Node's
resolver both stat on every fresh clone.

Root-level images are ignored the same way, since screenshots taken while
working are exactly what a broad `git add -A` picks up. Scoped to the
root so the real assets under */public/ stay tracked.

* fix: generate the ui mirror before tests, and cover the registry routes

CI was red and my earlier local runs were not evidence: the mirror in
website/components/ui is gitignored and only webjs.dev.before,
webjs.start.before, and the Dockerfile bake create it, so /ui/button
500'd in every job that boots the app. It passed locally only because a
dev server had already produced it. A pretest hook in both the root and
the website package now generates it, and both suites pass from a tree
with the mirror deleted.

The registry endpoints had no test at their new location: the redirect
test asserted Location strings, so a correct redirect into a broken
endpoint would have shipped green. Now the manifest, the flat index, an
item's inlined source, the reserved index/registry slugs, CORS, and the
static-over-dynamic precedence are all asserted.

Also: the mirror writes only changed files, so a stylesheet regeneration
no longer restarts the dev server through its own watch path; the
registry lib dir joins the regenerate inputs (it owns cn and the layout
helpers every example imports); and the dark variant is pinned, since it
is inert outside previews and nothing else would catch its removal.

* perf(website): elide the shell carriers and drop dead action seeds

Two findings from review, both measured rather than reasoned about.

The shared shell's inline onclick attributes made lib/docs-shell.ts
client-effecting, which shipped BOTH layout modules to the browser
against the execution model's own self-check. The handlers move into the
root layout's existing delegated listener, which already survives
navigation, so both layouts now elide. That also fixes a stale
aria-expanded: dismissing the drawer via the backdrop needs no
navigation and used to leave the toggle advertising an expanded drawer,
and Escape now closes it and restores focus.

SSR action seeding was serializing every registry read into the page for
a consumer that cannot exist: no component here does an async render or
calls an action, and a page function never re-runs in the browser. It was
a quarter of some responses. /ui/dropdown-menu drops 141KB to 107KB and
the pre-existing /changelog 1.10MB to 800KB.

Also unpinned the snippet map and API metadata from the browser bundle
(a module-scope IIFE and one comment mentioning a DOM global were enough
to mark both client-effecting) and trimmed the page to only the Tier-2
side-effect imports, since a Tier-1 helper registers no element.

* fix(website): make the gallery's a11y match what it documents

The kit's own docs list scope on header cells and a caption as the
reader's Tier-1 obligation, and the gallery shipped neither: 2 bare th
on /ui and 6 on every component page. Worse, the paste-ready table
snippet in the Code tab taught the omission, contradicting the
component's JSDoc that spells out scope=col.

preview-tabs declared role=tablist with two role=tab buttons and no
aria-controls, no tabpanel, and no keyboard handling, so it announced as
tabs with no associated panel on 33 indexed pages. It now implements
what the roles promise: cross-linked panels, a roving tabindex so the
group is one tab stop, and Arrow / Home / End. Covered by browser tests,
since all of it is focus behaviour that only exists after upgrade;
neutering aria-controls and the roving tabindex reds two of them.

* fix(website): give each component page its own description, list them in llms.txt

All 33 gallery URLs shared one meta description inherited from the
layout, which is the duplicate-content shape this migration exists to
remove; introducing 33 of them in a search-consolidation change would
have been self-defeating. Each page now derives its own from the tier
and the naming convention, so it says something true and distinct and
stays correct if a component changes tier. The registry carries no
per-item description, so a hand-written map would just be 33 more
strings to drift.

llms.txt enumerated the 45 doc pages but not a single component, while
the sitemap advertised all 33. An agent entering through llms.txt could
not discover the kit it is meant to use.

Also repaired three stale pointers the move left behind: the favicon
generator wrote into a deleted directory and would have thrown, and the
registry README plus two source comments still credited the emptied app
as the composer.

* fix: name real helpers in gallery descriptions, drop the accordion underline

The derived helper name in each Tier-1 description was guessed from the
component name, and the convention does not hold for all of them:
breadcrumb exports breadcrumbListClass, popover exports
popoverContentClass, and switch exports switchInputClass, none of which
have a bare xClass. Three search snippets named a function that does not
exist. It now reads the exported name out of the component's own source,
so all 32 are true and stay true when the kit renames one.

The accordion trigger no longer underlines on hover. An underline reads
as a link affordance and this is a disclosure control on a summary
element; the pointer cursor and the rotating chevron already carry it.
Presentation only, so the shadcn API parity that matters is untouched.

* fix(website): correct two drawer interaction bugs found in review

Clicking the drawer toggle returned early before the header mobile
menu's outside-click close, so the header dropdown stayed open. The
header is a stacking context, so its menu was then trapped beneath the
drawer's backdrop: visible, dimmed, and unclickable. The menu
housekeeping now runs first, unconditionally.

Escape with both open closed both and left focus on the header summary
rather than the control that opened the drawer, contradicting the
comment right above it. The drawer branch returns now.

Verified in a real browser across all ten states, then pinned: six
browser tests for the interactions, plus a source assertion that the
transcribed listener still matches the real inline script, including the
ordering that actually broke.

* docs(website): correct the stated reason the drawer closes the header menu

The comment claimed the header menu was trapped under the drawer's
backdrop because the header is a stacking context. Measured on the live
page, that is backwards: the backdrop sits inside the root layout's z-1
context, so it composites below the z-20 header, and a hit test at a
dropdown link with the drawer open returns the link. The menu was never
unclickable, and docs-shell.ts already documents the real z-order, so
the repo carried two comments asserting opposite stacking.

The behaviour is right and unchanged; only the justification was wrong,
which is worse than no comment because the next reader reasons from it.

* feat(ui): dim the accordion trigger on hover instead of underlining

Replaces the affordance removed with the underline. A slight drop in
text weight reads as interactive without borrowing a link's vocabulary,
and it pairs with the chevron that rotates on open.

The label now carries text-foreground explicitly rather than inheriting
it, because the hover target needs a known starting colour: dimming an
author's inherited colour to 80% of the THEME foreground would shift the
hue rather than just the weight. Slash opacity is already idiomatic in
the kit (text-foreground/60, ring-ring/50), and Tailwind emits a solid
fallback ahead of the color-mix form.

Verified hovered in a real browser in both themes.

* style(website): restore the 1200/800 docs measure for /docs and /ui

#1101 widened the documentation from the 1200 container and 800 content
column it used on its own domain to 1240/820, and this branch inherited
that and then applied it to the gallery as well. Back to 1200/800 for
both, from the one shared shell.

A reference page is read line by line and 800px is the comfortable
ceiling for that measure; the extra 20px bought nothing. Note the shell
is now 40px narrower than the header's own container, so the sidebar
sits 20px inside the wordmark rather than flush with it. That is the
deliberate trade, recorded in the shell, and the header can follow if
the offset ever reads as an accident.

* style(website): keep the shell aligned to the header, cap the text at 800

Splits the two decisions #1101 had moved together. The outer container
goes back to the header's 1240 so the sidebar sits flush under the
wordmark, while the content column stays capped at 800, the comfortable
ceiling for a reference page read line by line.

The slack lands as space between the sidebar and the text rather than as
a longer line. Verified at 1600px: sidebar and wordmark both start at
204, content column 800, no horizontal scroll.

* revert(website): restore the original docs shell measure

Reverts the two width commits from this session, putting the container
back to 1240 and the content column to 820, byte-identical to what main
serves today.

Those changes were chasing a widening that happened in #1101, not here,
and the second of them (820 to 800) was a 20px difference nobody could
see. The shell is back to inheriting whatever the documentation uses, so
/docs is untouched by this PR and /ui matches it, which was the original
request.

* fix(website): put the source pane's scrollbar inside its card

The height cap and overflow sat on a wrapper div while the border,
background, and rounded corners come from the pre inside it, so the
scrollbar rendered outside the visible rectangle, detached from the card
it was scrolling. A scrollbar renders inside its own element's border
box, so both now live on the pre.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

seo: serve the docs at webjs.dev/docs with the marketing site chrome

1 participant